package ar.com.algotres.tpdos.model.aviones;

import static ar.com.algotres.tpdos.model.aviones.EstadoAvion.COLISIONADO;
import static ar.com.algotres.tpdos.model.aviones.EstadoAvion.ESPERANDO_MOVIMIENTO;
import static ar.com.algotres.tpdos.model.aviones.EstadoAvion.ESPERANDO_TRAYECTORIA;
import static ar.com.algotres.tpdos.model.aviones.EstadoAvion.VOLANDO;

import java.util.Deque;
import java.util.LinkedList;

import ar.com.algotres.tpdos.exceptions.AvionNoSeleccionadoException;
import ar.com.algotres.tpdos.model.aterrizable.Pista;
import ar.com.algotres.tpdos.model.controlador.ITorreDeControl;
import ar.com.algotres.tpdos.model.fisica.Movimiento;
import ar.com.algotres.tpdos.model.fisica.Posicion;
import ar.com.algotres.tpdos.model.fisica.RepresentacionFisicaRectangular;
import ar.com.algotres.tpdos.model.fisica.Trayectoria;
import ar.com.algotres.tpdos.model.fisica.Velocidad;
import ar.com.algotres.tpdos.model.mapas.Mapa;
import ar.com.algotres.tpdos.views.juego.IVistaJuego;
import ar.uba.fi.algo3.titiritero.Posicionable;

public abstract class Avion implements Posicionable {

	private CaracteristicasAvion caracteristicas;
	private Movimiento movimiento;
	private ITorreDeControl torreDeControl;
	private EstadoAvion estado;
	private Deque<Trayectoria> trayectorias;
	private RepresentacionFisicaRectangular representacionFisica;

	public Avion(ITorreDeControl torre, Movimiento movimiento,
			Posicion posicion, CaracteristicasAvion caracteristicas) {
		this.movimiento = movimiento;
		this.torreDeControl = torre;
		this.estado = EstadoAvion.VOLANDO;
		this.caracteristicas = caracteristicas;
		this.trayectorias = new LinkedList<Trayectoria>();
		this.representacionFisica = new RepresentacionFisicaRectangular(
				posicion);
	}

	public abstract void avionCreado(IVistaJuego vistaJuego);
	
	public abstract boolean puedeAterrizarEnPista(Pista pista);

	
	public void mover() {

		Posicion posicionActual = representacionFisica.getPosicion();

		// logica para cambiar de trayectoria cuando finaliza una TODO: extraer
		if (posicionActual.equals(movimiento.getTrayectoria()
				.getPosicionFinal()) && !trayectorias.isEmpty()) {
			Trayectoria trayectoria = this.trayectorias.poll();
			actualizarMovimiento(trayectoria);
		}

		if (posicionActual.distanciaA(movimiento.getTrayectoria()
				.getPosicionInicial()) > movimiento.getTrayectoria()
				.getPosicionInicial()
				.distanciaA(movimiento.getTrayectoria().getPosicionFinal())
				&& EstadoAvion.ESPERANDO_TRAYECTORIA.equals(estado)) {
			estado = EstadoAvion.ESPERANDO_MOVIMIENTO;
		}

		Double nuevaPosX = getDesplazamientoCorrectoEnX(movimiento
				.getTrayectoria().getPosicionFinal(),
				posicionActual.getXDouble() + movimiento.getDesplazamientoEnX());
		Double nuevaPosY = getDesplazamientoCorrectoEnY(movimiento
				.getTrayectoria().getPosicionFinal(),
				posicionActual.getYDouble() + movimiento.getDesplazamientoEnY());

		actualizarPosicion(new Posicion(nuevaPosX, nuevaPosY));

		intentarAterrizar();

		verificarPosicionEnMapa();

		verificarColision();

	}

	/**
	 * Verifica si el avion está sobre una pista en la que puede aterrizar. Si
	 * lo está, aterriza.
	 */
	private void intentarAterrizar() {
		if (tienePistaValida()) {
			aterrizar();
		}
	}

	/**
	 * Mantiene el avion dentro de los limites del mapa
	 */
	private void verificarPosicionEnMapa() {
		if (posicionFueraDeMapa()) {
			corregirPosicionActual();
			cambiarMovimientoPorMovimientoRandom();
		}
	}

	private void verificarColision() {
		if (colisiono()) {
			estado = COLISIONADO;
		}
	}

	protected void actualizarMovimiento(Trayectoria trayectoria) {
		movimiento = new Movimiento(this.movimiento.getVelocidad(), trayectoria);
	}
	
	protected void esperarMovimiento() {
		estado = EstadoAvion.ESPERANDO_MOVIMIENTO;
	}
	
	protected void actualizarMovimiento(Trayectoria trayectoria, Velocidad velocidad) {
		movimiento = new Movimiento(velocidad, trayectoria);
	}

	/**
	 * Modifica el movimiento del avion al alcanzar un limite del mapa. Simula
	 * un rebote.
	 */
	private void cambiarMovimientoPorMovimientoRandom() {
		Posicion nuevaPosFinal = torreDeControl.getMapa().randomPosicion();
		Trayectoria nuevaTrayectoria = new Trayectoria(
				representacionFisica.getPosicion(), nuevaPosFinal);
		actualizarMovimiento(nuevaTrayectoria);
	}

	/**
	 * Retrocede los desplazamientos del avion para retornarlo al mapa.
	 */
	private void corregirPosicionActual() {
		Posicion posicionActual = representacionFisica.getPosicion();
		Double nuevaX = posicionActual.getXDouble();
		Double nuevaY = posicionActual.getYDouble();

		if (componenteXFueraDeMapa()) {
			nuevaX = (posicionActual.getXDouble() - movimiento
					.getDesplazamientoEnX());
		}
		if (componenteYFueraDeMapa()) {
			nuevaY = (posicionActual.getYDouble() - movimiento
					.getDesplazamientoEnY());
		}

		actualizarPosicion(new Posicion(nuevaX, nuevaY));

		if (posicionFueraDeMapa()) {
			corregirPosicionActual();
		}
	}

	private boolean componenteXFueraDeMapa() {
		Posicion posicion = representacionFisica.getPosicion();
		return (posicion.getX() < Mapa.CERO)
				|| ((posicion.getX() + RepresentacionFisicaRectangular.ANCHO) > Mapa.DIMENSION_EN_X);
	}

	private boolean componenteYFueraDeMapa() {
		Posicion posicion = representacionFisica.getPosicion();
		return (posicion.getY() < Mapa.CERO)
				|| ((posicion.getY() + RepresentacionFisicaRectangular.ALTO) > Mapa.DIMENSION_EN_Y);
	}

	private boolean posicionFueraDeMapa() {
		return componenteXFueraDeMapa() || componenteYFueraDeMapa();
	}

	/**
	 * Actualiza la posicion del avion y el objeto colisionable
	 * 
	 * @param nuevaPosicion
	 */
	private void actualizarPosicion(Posicion nuevaPosicion) {
		representacionFisica.setPosicion(nuevaPosicion);
	}

	private double getDesplazamientoCorrectoEnX(Posicion posicionFinal,
			double desplazamientoX) {
		return this.esDesplazamientoCorrectoX(posicionFinal, desplazamientoX) ? desplazamientoX
				: posicionFinal.getX();
	}

	private double getDesplazamientoCorrectoEnY(Posicion posicionFinal,
			double desplazamientoY) {
		return this.esDesplazamientoCorrectoY(posicionFinal, desplazamientoY) ? desplazamientoY
				: posicionFinal.getY();
	}

	private Boolean esDesplazamientoCorrectoX(Posicion posicionFinal,
			double desplazamientoX) {
		Double deltaMovimiento = Math.abs(movimiento.getDesplazamientoEnX());
		Double deltaX = Math.abs(posicionFinal.getX()
				- representacionFisica.getPosicion().getXDouble());
		// si se le acaban las trayectorias, el desplazamiento deberia ser el
		// dado por el movimiento
		return deltaMovimiento < deltaX || trayectorias.isEmpty();
	}

	private Boolean esDesplazamientoCorrectoY(Posicion posicionFinal,
			double desplazamientoY) {
		Double deltaMovimiento = Math.abs(movimiento.getDesplazamientoEnY());
		Double deltaY = Math
				.abs((double) (posicionFinal.getY() - representacionFisica
						.getY()));
		// si se le acaban las trayectorias, el desplazamiento deberia ser el
		// dado por el movimiento
		return deltaMovimiento < deltaY || trayectorias.isEmpty();
	}

	public Deque<Trayectoria> getTrayectorias() {
		return trayectorias;
	}

	public void setTrayectorias(LinkedList<Trayectoria> trayectorias) {
		this.trayectorias = trayectorias;
	}

	public Movimiento getMovimiento() {
		return movimiento;
	}

	public Posicion getPosicion() {
		return representacionFisica.getPosicion();
	}

	public ITorreDeControl getTorreDeControl() {
		return torreDeControl;
	}

	public EstadoAvion getEstado() {
		return estado;
	}

	public CaracteristicasAvion getCaracteristicas() {
		return caracteristicas;
	}

	@Override
	public String toString() {
		return "Estado: " + estado.name() + " | Posición: X = "
				+ representacionFisica.getPosicion().getX() + " ; Y = "
				+ representacionFisica.getPosicion().getY();
	}

	public boolean tienePistaValida() {
		return torreDeControl.tienePistaValida(this);
	}

	public void aterrizar() {
		estado = EstadoAvion.ATERRIZADO;
	}

	public void agregarTrayectoria(Trayectoria trayectoria) {
		if (EstadoAvion.ESPERANDO_MOVIMIENTO.equals(estado)) {
			actualizarMovimiento(trayectoria);
			estado = EstadoAvion.ESPERANDO_TRAYECTORIA;
		} else if (EstadoAvion.ESPERANDO_TRAYECTORIA.equals(estado)) {
			trayectorias.add(trayectoria);
		}
	}

	@Override
	public int getX() {
		return representacionFisica.getPosicion().getX();
	}

	@Override
	public int getY() {
		return representacionFisica.getPosicion().getY();
	}

	public boolean isAterrizado() {
		return EstadoAvion.ATERRIZADO.equals(estado);
	}

	/**
	 * Verifica si el avion chocó con otro avion.
	 * 
	 * @param otroAvion
	 * @return
	 */
	public Boolean colisionaCon(Avion otroAvion) {
		return representacionFisica.colisionaCon(otroAvion
				.getRepresentacionFisica());
	}

	public Boolean colisiono() {
		for (Avion otroAvion : torreDeControl.getAviones()) {
			if (this.colisionaCon(otroAvion) && (!this.equals(otroAvion))) {
				return true;
			}
		}
		return false;
	}

	public RepresentacionFisicaRectangular getRepresentacionFisica() {
		return representacionFisica;
	}

	/**
	 * Limpia la lista de trayectorias y pone el avion en espera de nuevas
	 * trayectorias
	 */
	public void seleccionar() {
		trayectorias.clear();
		estado = ESPERANDO_MOVIMIENTO;
	}

	public void deseleccionar() {
		if(!seleccionado()) {
			throw new AvionNoSeleccionadoException("No se puede deseleccionar un avion que no fue seleccionado previamente");
		}
		estado = VOLANDO;
	}

	public Boolean seleccionado() {
		return ESPERANDO_MOVIMIENTO.equals(estado)
				|| ESPERANDO_TRAYECTORIA.equals(estado);
	}

	public Posicion getFinalDeRuta() {
		if (ESPERANDO_TRAYECTORIA.equals(estado)) {
			if (trayectorias.isEmpty()) {
				return movimiento.getTrayectoria().getPosicionFinal();
			} else {
				return trayectorias.getLast().getPosicionFinal();
			}
		}
		return getPosicion();
	}
}
